package com.snoob.springcloudalibaba.common.tenxun.util;

import com.alibaba.fastjson.JSONObject;
import com.github.wxpay.sdk.WXPayConstants;
import com.github.wxpay.sdk.WXPayConstants.SignType;
import com.github.wxpay.sdk.WXPayXmlUtil;
import com.snoob.springcloudalibaba.base.BaseResult;
import com.snoob.springcloudalibaba.common.pay.util.AdvancePaymentProperties;
import com.snoob.springcloudalibaba.util.HttpHelperUtil;
import com.snoob.springcloudalibaba.util.RequestUtil;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.*;

/**
 * @描述 微信支付相关工具类
 * @作者 lixing
 * @日期 2021/7/10 10:21
 */
public class WxPayUtil {
    private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
    private static final Random RANDOM = new SecureRandom();

    /**
     * XML格式字符串转换为Map
     */
    public static Map<String, String> xmlToMap(String strXML) throws Exception {
        try {
            Map<String, String> data = new HashMap<String, String>();
            DocumentBuilder documentBuilder = WXPayXmlUtil.newDocumentBuilder();
            InputStream stream = new ByteArrayInputStream(strXML.getBytes(StandardCharsets.UTF_8));
            org.w3c.dom.Document doc = documentBuilder.parse(stream);
            doc.getDocumentElement().normalize();
            NodeList nodeList = doc.getDocumentElement().getChildNodes();
            for (int idx = 0; idx < nodeList.getLength(); ++idx) {
                Node node = nodeList.item(idx);
                if (node.getNodeType() == Node.ELEMENT_NODE) {
                    org.w3c.dom.Element element = (org.w3c.dom.Element) node;
                    data.put(element.getNodeName(), element.getTextContent());
                }
            }
            try {
                stream.close();
            } catch (Exception ex) {
                // do nothing
            }
            return data;
        } catch (Exception ex) {
            throw ex;
        }

    }

    /**
     * 将Map转换为XML格式的字符串
     */
    public static String mapToXml(Map<String, String> data) throws Exception {
        org.w3c.dom.Document document = WXPayXmlUtil.newDocument();
        org.w3c.dom.Element root = document.createElement("xml");
        document.appendChild(root);
        for (String key : data.keySet()) {
            String value = data.get(key);
            if (value == null) {
                value = "";
            }
            value = value.trim();
            org.w3c.dom.Element filed = document.createElement(key);
            filed.appendChild(document.createTextNode(value));
            root.appendChild(filed);
        }
        TransformerFactory tf = TransformerFactory.newInstance();
        Transformer transformer = tf.newTransformer();
        DOMSource source = new DOMSource(document);
        transformer.setOutputProperty(OutputKeys.ENCODING, StandardCharsets.UTF_8.toString());
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        StringWriter writer = new StringWriter();
        StreamResult result = new StreamResult(writer);
        transformer.transform(source, result);
        String output = writer.getBuffer().toString(); //.replaceAll("\n|\r", "");
        try {
            writer.close();
        } catch (Exception ex) {
        }
        return output;
    }

    /**
     * 生成带有 sign 的 XML 格式字符串
     * @param data Map类型数据
     * @param key  API密钥
     * @return 含有sign字段的XML
     */
    public static String generateSignedXml(final Map<String, String> data, String key) throws Exception {
        return generateSignedXml(data, key, SignType.MD5);
    }

    /**
     * 生成带有 sign 的 XML 格式字符串
     * @param data     Map类型数据
     * @param key      API密钥
     * @param signType 签名类型
     * @return 含有sign字段的XML
     */
    public static String generateSignedXml(final Map<String, String> data, String key, SignType signType) throws Exception {
        String sign = generateSignature(data, key, signType);
        data.put(WXPayConstants.FIELD_SIGN, sign);
        return mapToXml(data);
    }

    /**
     * 判断签名是否正确
     * @param xmlStr XML格式数据
     * @param key    API密钥
     * @return 签名是否正确
     */
    public static boolean isSignatureValid(String xmlStr, String key) throws Exception {
        Map<String, String> data = xmlToMap(xmlStr);
        if (!data.containsKey(WXPayConstants.FIELD_SIGN)) {
            return false;
        }
        String sign = data.get(WXPayConstants.FIELD_SIGN);
        return generateSignature(data, key).equals(sign);
    }

    /**
     * 判断签名是否正确，必须包含sign字段，否则返回false。使用MD5签名。
     * @param data Map类型数据
     * @param key  API密钥
     * @return 签名是否正确
     */
    public static boolean isSignatureValid(Map<String, String> data, String key) throws Exception {
        return isSignatureValid(data, key, SignType.MD5);
    }

    /**
     * 判断签名是否正确，必须包含sign字段，否则返回false。
     * @param data     Map类型数据
     * @param key      API密钥
     * @param signType 签名方式
     * @return 签名是否正确
     */
    public static boolean isSignatureValid(Map<String, String> data, String key, SignType signType) throws Exception {
        if (!data.containsKey(WXPayConstants.FIELD_SIGN)) {
            return false;
        }
        String sign = data.get(WXPayConstants.FIELD_SIGN);
        return generateSignature(data, key, signType).equals(sign);
    }

    /**
     * 生成签名
     * @param data 待签名数据
     * @param key  API密钥
     * @return 签名
     */
    public static String generateSignature(final Map<String, String> data, String key) throws Exception {
        return generateSignature(data, key, SignType.MD5);
    }

    /**
     * 生成签名. 注意，若含有sign_type字段，必须和signType参数保持一致。
     * @param data     待签名数据
     * @param key      API密钥
     * @param signType 签名方式
     * @return 签名
     */
    public static String generateSignature(final Map<String, String> data, String key, SignType signType) throws Exception {
        Set<String> keySet = data.keySet();
        String[] keyArray = keySet.toArray(new String[keySet.size()]);
        Arrays.sort(keyArray);
        StringBuilder sb = new StringBuilder();
        for (String k : keyArray) {
            if (k.equals(WXPayConstants.FIELD_SIGN)) {
                continue;
            }
            // 参数值为空，则不参与签名
            if (data.get(k).trim().length() > 0) {
                sb.append(k).append("=").append(data.get(k).trim()).append("&");
            }
        }
        sb.append("key=").append(key);
        if (SignType.MD5.equals(signType)) {
            return MD5(sb.toString()).toUpperCase();
        } else if (SignType.HMACSHA256.equals(signType)) {
            return HMACSHA256(sb.toString(), key);
        } else {
            throw new Exception(String.format("Invalid sign_type: %s", signType));
        }
    }

    /**
     * 获取32位随机字符串
     */
    public static String generateNonceStr() {
        char[] nonceChars = new char[32];
        for (int index = 0; index < nonceChars.length; ++index) {
            nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length()));
        }
        return new String(nonceChars);
    }

    /**
     * 生成 MD5
     *
     * @param data 待处理数据
     * @return MD5结果
     */
    public static String MD5(String data) throws Exception {
        MessageDigest md = MessageDigest.getInstance("MD5");
        byte[] array = md.digest(data.getBytes(StandardCharsets.UTF_8));
        StringBuilder sb = new StringBuilder();
        for (byte item : array) {
            sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
        }
        return sb.toString().toUpperCase();
    }

    /**
     * 生成 HMACSHA256
     *
     * @param data 待处理数据
     * @param key  密钥
     */
    public static String HMACSHA256(String data, String key) throws Exception {
        Mac sha256HMAC = Mac.getInstance("HmacSHA256");
        SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
        sha256HMAC.init(secretKey);
        byte[] array = sha256HMAC.doFinal(data.getBytes(StandardCharsets.UTF_8));
        StringBuilder sb = new StringBuilder();
        for (byte item : array) {
            sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
        }
        return sb.toString().toUpperCase();
    }

    /**
     * 获取当前时间戳，单位秒
     */
    public static long getCurrentTimestamp() {
        return System.currentTimeMillis() / 1000;
    }

    /**
     * 获取当前时间戳，单位毫秒
     */
    public static long getCurrentTimestampMs() {
        return System.currentTimeMillis();
    }

    /**
     * @描述 统一支付（预支付）
     * @Param [useSandbox 是否使用沙箱环境，reqData 请求参数]
     * @return java.util.Map<java.lang.String,java.lang.String>
     */
    public static Map<String, String> unifiedOrderForAggregatePayment(HttpServletRequest request, AdvancePaymentProperties advancePaymentProperties) throws Exception {
        // 默认使用MD5
        SignType signType = SignType.MD5;
        /*
         * 封装参数
         */
        SortedMap<String,String> mapParam = new TreeMap<>();
        // TODO 非必填
        // 商品ID
        // mapParam.put("product_id", advancePaymentProperties.getProductId());
        // TODO 必填
        // 订单总金额，单位为分
        mapParam.put("total_fee", advancePaymentProperties.getTotalAmount());
        // 商户订单号
        mapParam.put("out_trade_no", advancePaymentProperties.getOrderNo());
        // 商品描述
        mapParam.put("body", advancePaymentProperties.getBody());
        // 随机字符串，最大长度32
        mapParam.put("nonce_str", WxPayUtil.generateNonceStr());
        // 终端IP
        mapParam.put("spbill_create_ip", RequestUtil.getIpAddress(request));
        // appid
        mapParam.put("appid", advancePaymentProperties.getWechatAppId());
        // 商户号
        mapParam.put("mch_id", advancePaymentProperties.getWechatMchId());
        // 签名类型(非必填)，默认为MD5，支持HMAC-SHA256和MD5
        mapParam.put("sign_type", signType.name());
        // 交易类型，小程序默认使用JSAPI
        mapParam.put("trade_type", "JSAPI");
        // trade_type=JSAPI时（即JSAPI支付），此参数必传，此参数为微信用户在商户对应appid下的唯一标识
        mapParam.put("openid", advancePaymentProperties.getWechatOpenId());
        // 支付成功后的回调地址。必须为外网可访问的url，不能携带参数
        mapParam.put("notify_url", advancePaymentProperties.getWechatNotifyUrl());
        // 商家数据包，原样返回
        mapParam.put("attach", advancePaymentProperties.getPassbackParams());
        // 通过签名算法计算得出的签名值
        String sign = WxPayUtil.generateSignature(mapParam, advancePaymentProperties.getWechatKey(), signType);
        mapParam.put("sign", sign);
        /*
         * 发起请求
         */
        String xmlParam = WxPayUtil.mapToXml(mapParam);
        String resultXml = HttpHelperUtil.sendRequestHttp("https://api.mch.weixin.qq.com/pay/unifiedorder", "POST", xmlParam);
        /*
         * https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1
         * return_code 为SUCCESS时返回以下字段：appid、mch_id、device_info、nonce_str、sign、result_code、err_code、err_code_des
         * return_code 和 result_code 都为SUCCESS的时候返回以下字段：trade_type、prepay_id、code_url
         *
         * {nonce_str=QS5vT6pKCRpJw7F8, appid=wx18cda9dfbce1d8b8, sign=53C05B36715FA59C723A26F3B6036DBB, trade_type=JSAPI, return_msg=OK, result_code=SUCCESS, mch_id=1611059987, return_code=SUCCESS, prepay_id=wx0713054610783747ca6510e80181840000}
         */
        Map<String, String> resultMap = WxPayUtil.xmlToMap(resultXml);
        if (resultMap.get("return_code").equals(WXPayConstants.FAIL)) {
            return resultMap;
        }
        boolean isResponseSignatureValid = WxPayUtil.isSignatureValid(resultMap, advancePaymentProperties.getWechatKey(), signType);
        if (!isResponseSignatureValid) {
            // 校验返回数据的签名方式和请求中给定的签名方式是一致的
            resultMap.put("return_msg", "预支付失败");
        }
        return resultMap;
    }

    /**
     * @描述 从微信统一支付的结果中获取页面唤起JSAPI调起支付所需的参数
     * @Param [unifiedOrderResult] 
     * @return java.util.Map<java.lang.String,java.lang.String>
     */
    public static Map<String, String> getJsapiTransferPaymentParamForAggregatePayment(String prepayId, AdvancePaymentProperties advancePaymentProperties) throws Exception {
        String timeStamp = String.valueOf(WxPayUtil.getCurrentTimestamp());
        String nonceStr = WxPayUtil.generateNonceStr();
        SignType signType = SignType.MD5;
        // 统一下单接口返回的 prepay_id 参数值，提交格式如：prepay_id=*
        String packageInfo = "prepay_id=" + prepayId;
        // 生成签名
        SortedMap<String,String> paySignParam = new TreeMap<>();
        paySignParam.put("appId", advancePaymentProperties.getWechatAppId());
        paySignParam.put("timeStamp", timeStamp);
        paySignParam.put("nonceStr", nonceStr);
        paySignParam.put("package", packageInfo);
        paySignParam.put("signType", signType.name());
        String paySign = WxPayUtil.generateSignature(paySignParam, advancePaymentProperties.getWechatKey(), signType);
        //
        Map<String, String> jsapiTransferPaymentParam = new HashMap<>(6);
        jsapiTransferPaymentParam.put("appId", advancePaymentProperties.getWechatAppId());
        jsapiTransferPaymentParam.put("timeStamp", timeStamp);
        jsapiTransferPaymentParam.put("nonceStr", nonceStr);
        jsapiTransferPaymentParam.put("package", packageInfo);
        jsapiTransferPaymentParam.put("signType", signType.name());
        jsapiTransferPaymentParam.put("paySign", paySign);
        // 自定义-支付标题
        jsapiTransferPaymentParam.put("subject", advancePaymentProperties.getSubject());
        return jsapiTransferPaymentParam;
    }

    /**
     * @描述 统一支付【普通支付】
     * @Param [useSandbox 是否使用沙箱环境，reqData 请求参数]
     * @return java.util.Map<java.lang.String,java.lang.String>
     */
    public static BaseResult<Object> unifiedOrder(JSONObject reqParamJson) throws Exception {
        // 默认使用MD5
        SignType signType = SignType.MD5;
        /*
         * 封装参数
         */
        SortedMap<String,String> mapParam = new TreeMap<>();
        // TODO 非必填
        // 商品ID
        // mapParam.put("product_id", advancePaymentProperties.getProductId());
        // TODO 必填
        // 订单总金额，单位为分
        mapParam.put("total_fee", reqParamJson.getString("totalFee"));
        // 商户订单号
        mapParam.put("out_trade_no", reqParamJson.getString("outTradeNo"));
        // 商品描述
        mapParam.put("body", reqParamJson.getString("body"));
        // 随机字符串，最大长度32
        mapParam.put("nonce_str", WxPayUtil.generateNonceStr());
        // 终端IP
        mapParam.put("spbill_create_ip", reqParamJson.getString("spbillCreateIp"));
        // appid
        mapParam.put("appid", reqParamJson.getString("appId"));
        // 商户号
        mapParam.put("mch_id", reqParamJson.getString("mchId"));
        // 签名类型(非必填)，默认为MD5，支持HMAC-SHA256和MD5
        mapParam.put("sign_type", signType.name());
        // 交易类型，小程序默认使用JSAPI
        mapParam.put("trade_type", "JSAPI");
        // trade_type=JSAPI时（即JSAPI支付），此参数必传，此参数为微信用户在商户对应appid下的唯一标识
        mapParam.put("openid", reqParamJson.getString("openid"));
        // 支付成功后的回调地址。必须为外网可访问的url，不能携带参数
        mapParam.put("notify_url", reqParamJson.getString("notifyUrl"));
        // 商家数据包，原样返回
        mapParam.put("attach", reqParamJson.getString("attach"));
        // 通过签名算法计算得出的签名值
        String sign = WxPayUtil.generateSignature(mapParam, reqParamJson.getString("key"), signType);
        mapParam.put("sign", sign);
        /*
         * 发起请求
         */
        String xmlParam = WxPayUtil.mapToXml(mapParam);
        String resultXml = HttpHelperUtil.sendRequestHttp("https://api.mch.weixin.qq.com/pay/unifiedorder", "POST", xmlParam);
        /*
         * https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1
         * return_code 为SUCCESS时返回以下字段：appid、mch_id、device_info、nonce_str、sign、result_code、err_code、err_code_des
         * return_code 和 result_code 都为SUCCESS的时候返回以下字段：trade_type、prepay_id、code_url
         *
         * {nonce_str=QS5vT6pKCRpJw7F8, appid=wx18cda9dfbce1d8b8, sign=53C05B36715FA59C723A26F3B6036DBB, trade_type=JSAPI, return_msg=OK, result_code=SUCCESS, mch_id=1611059987, return_code=SUCCESS, prepay_id=wx0713054610783747ca6510e80181840000}
         */
        Map<String, String> resultMap = WxPayUtil.xmlToMap(resultXml);
        if (resultMap.get("return_code").equals(WXPayConstants.FAIL)) {
            return new BaseResult<Object>().success(resultMap.get("return_msg"));
        }
        boolean isResponseSignatureValid = WxPayUtil.isSignatureValid(resultMap, reqParamJson.getString("key"), signType);
        if (!isResponseSignatureValid) {
            return new BaseResult<Object>().fail("签名非法");
        }
        /*
         * 生成预支付信息
         */
        String timeStamp = String.valueOf(WxPayUtil.getCurrentTimestamp());
        String nonceStr = WxPayUtil.generateNonceStr();
        // 统一下单接口返回的 prepay_id 参数值，提交格式如：prepay_id=*
        String packageInfo = "prepay_id=" + resultMap.get("prepay_id");
        // 生成签名
        SortedMap<String,String> paySignParam = new TreeMap<>();
        paySignParam.put("appId", reqParamJson.getString("appId"));
        paySignParam.put("timeStamp", timeStamp);
        paySignParam.put("nonceStr", nonceStr);
        paySignParam.put("package", packageInfo);
        paySignParam.put("signType", signType.name());
        String paySign = WxPayUtil.generateSignature(paySignParam, reqParamJson.getString("key"), signType);
        //
        Map<String, String> jsapiTransferPaymentParam = new HashMap<>(6);
        jsapiTransferPaymentParam.put("appId", reqParamJson.getString("appId"));
        jsapiTransferPaymentParam.put("timeStamp", timeStamp);
        jsapiTransferPaymentParam.put("nonceStr", nonceStr);
        jsapiTransferPaymentParam.put("package", packageInfo);
        jsapiTransferPaymentParam.put("signType", signType.name());
        jsapiTransferPaymentParam.put("paySign", paySign);
        return new BaseResult<Object>().success(jsapiTransferPaymentParam);
    }

}
